﻿//Copyright © Steffen Zeidler 2010
using System.Collections.Generic;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;

namespace System.Threading
{
    /// <summary>
    /// Scheduler with virtualized time
    /// single threaded
    /// uses SynchronizationContext
    /// </summary>
    public class TimeMachineScheduler2 : IScheduler
    {
        public IDisposable Schedule<TState>(TState state, DateTimeOffset dueTime, Func<IScheduler, TState, IDisposable> action)
        {
            if (!(SynchronizationContext.Current is SynchronizationContext2))
            {
                throw new NotSupportedException("call from other thread");
            }
            //insert the action in the time sorted list
            int i = 0;
            for (; i < actions.Count && dueTime >= actions[i].time; i++) ;
            var entry = new Item<TState>() { time = dueTime, scheduler = this, state = state, action = action };
            actions.Insert(i, entry);
            return Disposable.Create(() => 
            {
                actions.Remove(entry);
            });
        }

        public IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action)
        {
            return Schedule(state, virtualTime + dueTime, action);
        }

        public IDisposable Schedule<TState>(TState state, Func<IScheduler, TState, IDisposable> action)
        {
            return Schedule(state, TimeSpan.Zero, action);
        }

        public DateTimeOffset Now { get { return virtualTime; } }

        public static readonly TimeMachineScheduler2 instance = new TimeMachineScheduler2();

        private TimeMachineScheduler2()
        {
            if (SynchronizationContext.Current != null)
            {
                throw new NotSupportedException("other SynchronizationContext");
            }
            SynchronizationContext.SetSynchronizationContext(new SynchronizationContext2());
        }

        //scheduler
        private bool Run()
        {
            if (actions.Count > 0)
            {
                if (virtualTime < actions[0].time)
                {
                    //set the next virtual time
                    virtualTime = actions[0].time;
                }
                Action action = () => actions[0].Run();
                actions.RemoveAt(0);
                action();
                Thread.Sleep(1); //HACK
                return true;
            }
            return false;
        }

        private abstract class Item
        {
            public DateTimeOffset time;
            public abstract IDisposable Run();
        }

        private class Item<TState> : Item
        {
            public TState state;
            public IScheduler scheduler;
            public Func<IScheduler, TState, IDisposable> action;
            public override IDisposable Run()
            {
                return action(scheduler, state);
            }
        }

        //time sorted action list
        private List<Item> actions = new List<Item>();
        //current virtual time
        private DateTimeOffset virtualTime = DateTimeOffset.MinValue;

        private class SynchronizationContext2 : SynchronizationContext
        {
            public SynchronizationContext2()
            {
                SetWaitNotificationRequired();
            }

            public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
            {
                //the scheduler runs only if the thread is waiting
                while (true)
                {
                    int ret = base.Wait(waitHandles, waitAll, 0);
                    if (ret == WaitHandle.WaitTimeout)
                    {
                        if (!instance.Run())
                        {
                            //waiting with a real timeout should only the case outside of Rx
                            return base.Wait(waitHandles, waitAll, millisecondsTimeout);
                        }
                    }
                    else
                    {
                        return ret;
                    }
                }
            }
        }
    }
}
